﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Istace.Framework.Mapping.Core.Exceptions;
using Istace.Framework.Mapping.Core.Interfaces;

namespace Istace.Framework.Mapping.Core.Internal
{
    /// <summary>
    /// Service used to map object from different types
    /// </summary>
    internal sealed class MapperService : IMapperService
    {
        #region Singleton pattern
        private static IMapperService _instance;
        /// <summary>
        /// Default MapperService instance.
        /// </summary>
        public static IMapperService Instance
        {
            get { return _instance ?? (_instance = new MapperService()); }
        }
        #endregion

        #region Private properties
        /// <summary>
        /// List of the mappers available
        /// </summary>
        private readonly Dictionary<string, IMapper> Mappers;

        /// <summary>
        /// The map method reference
        /// </summary>
        private readonly MethodInfo MapMethodInfo;
        #endregion

        #region Private .ctor
        /// <summary>
        /// Default .ctor
        /// </summary>
        private MapperService()
        {
            this.Mappers = new Dictionary<string, IMapper>();
            this.MapMethodInfo = typeof(MapperService).GetMethods()
                                                             .FirstOrDefault(method =>
                                                                 method.Name.Equals("Map") &&
                                                                 method.IsGenericMethod &&
                                                                 method.GetGenericArguments().Count() == 2);
        }
        #endregion

        #region Public methods
        #region Registration methods
        /// <summary>
        /// Register a IMapper
        /// </summary>
        /// <param name="mapper">mapper instance</param>
        public void Register(IMapper mapper)
        {
            if(ReferenceEquals(mapper, null)) throw new ArgumentNullException("mapper");

            foreach(var tuple in MappingFrameworkBootstrapper.Instance.GetTupleFromMapperType(mapper.GetType()))
                this.Register(mapper, tuple);
        }

        /// <summary>
        /// Register a IMapper which implement the given MappingTuple
        /// </summary>
        /// <param name="mapper">Mapper Instance</param>
        /// <param name="tuple">Mapping tuple implmented by the mapper</param>
        /// <exception cref="System.ArgumentNullException"> mapper or tuple </exception>
        /// <exception cref="MapperAlreadyRegisteredException"></exception>
        public void Register(IMapper mapper, IMappingTuple tuple)
        {
            if (ReferenceEquals(mapper, null)) throw new ArgumentNullException("mapper");
            if (ReferenceEquals(tuple, null))  throw new ArgumentNullException("tuple");

            if (!this.Mappers.ContainsKey(tuple.ToString()))
            {
                this.Mappers.Add(tuple.ToString(), mapper);
            }
            else
            {
                throw new MapperAlreadyRegisteredException(tuple.Source, tuple.Destination);
            }
        }

        /// <summary>
        /// Register a IMapper which implement the given MappingTuple
        /// </summary>
        /// <param name="mapper">Mapper Instance</param>
        /// <param name="tuple">Mapping tuple implmented by the mapper</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentNullException"> mapper or tuple </exception>
        public bool TryRegister(IMapper mapper, IMappingTuple tuple)
        {
            if (ReferenceEquals(mapper, null)) return false;
            if (ReferenceEquals(tuple, null))  return false;

            if (!this.Mappers.ContainsKey(tuple.ToString()))
            {
                this.Mappers.Add(tuple.ToString(), mapper);
                return true;
            }
            return false;
        }
        #endregion

        /// <summary>
        /// Map the TSource source object to the TDestination type
        /// </summary>
        /// <typeparam name="TSource">TSource type</typeparam>
        /// <typeparam name="TDestination">Destination type</typeparam>
        /// <param name="source">Object to convert of TSsource type</param>
        /// <returns> Converted object to TDestination type </returns>
        /// <exception cref="MapperNotFoundException"></exception>
        public TDestination Map<TSource, TDestination>(TSource source)
        {
            if (ReferenceEquals(source,null)) throw new ArgumentNullException("source");

            IMapper mapper = GetMapperForGivenTypes(typeof (TSource), typeof (TDestination));
            IMapper<TSource, TDestination> genericMapper = mapper as IMapper<TSource, TDestination>;
            return genericMapper.Map(source);
        }

        /// <summary>
        /// Map the TSource type source object to the TDestination type
        /// </summary>
        /// <typeparam name="TDestination">TDestination type</typeparam>
        /// <param name="source">Object to convert of TSource type</param>
        /// <returns>
        /// Converted object to TDestination type
        /// </returns>
        /// <exception cref="System.ArgumentNullException">source</exception>
        /// <exception cref="MapperNotFoundException"></exception>
        public TDestination Map<TDestination>(object source)
        {
            if (ReferenceEquals(source, null)) throw new ArgumentNullException("source");
            // Double check but required. Without that the exception are caste to TargetInvocationFailed... Have to find a solution !
            GetMapperForGivenTypes(source.GetType(), typeof (TDestination));
            
            MethodInfo mappingMethod = GetMapMethodInfo(source.GetType(), typeof(TDestination));
            
            return (TDestination)mappingMethod.Invoke(this, new[] {source});
        }

        /// <summary>
        /// Map the TSource type source list object to the TDestination type
        /// </summary>
        /// <typeparam name="TSource">TSource type</typeparam>
        /// <typeparam name="TDestination">TDestination type</typeparam>
        /// <param name="source">Object list to convert of TSource type to TDestination type</param>
        /// <returns> Converted list of object to TDestination type</returns>
        /// <exception cref="System.ArgumentNullException">source</exception>
        /// <exception cref="MapperNotFoundException"></exception>
        public IEnumerable<TDestination> MapList<TSource, TDestination>(IEnumerable<TSource> source)
        {
            if (ReferenceEquals(source, null)) throw new ArgumentNullException("source");

            return source.Select(Map<TSource, TDestination>);
        }

        /// <summary>
        /// Map the source object list to the TDestination type
        /// </summary>
        /// <typeparam name="TDestination">TDestination type</typeparam>
        /// <param name="source">Object list to convert of TSource type to TDestination type</param>
        /// <returns>
        /// Converted list of object to TDestination type
        /// </returns>
        /// <exception cref="System.ArgumentNullException">source</exception>
        /// <exception cref="MapperNotFoundException"></exception>
        public IEnumerable<TDestination> MapList<TDestination>(IEnumerable source)
        {
            if (ReferenceEquals(source, null)) throw new ArgumentNullException("source");
            
            foreach(var obj in source)
                yield return this.Map<TDestination>(obj);
        }

        #endregion
        private IMapper GetMapperForGivenTypes(Type sourceType, Type destinationType)
        {
            MappingTuple tuple;
            
            bool browseDeeper;
            // Browse for the "concrete types"
            do
            {
                browseDeeper = false;
                tuple = new MappingTuple(sourceType, destinationType);
                if (this.Mappers.ContainsKey(tuple.ToString()))
                    return this.Mappers[tuple.ToString()];

                if (sourceType.BaseType != null)
                {
                    sourceType = sourceType.BaseType;
                    browseDeeper = true;
                }
            } while (browseDeeper);

            // Browse the interfaces
            foreach(var declaringInterface in sourceType.GetInterfaces().ToList())
            {
                tuple = new MappingTuple(declaringInterface, destinationType);
                if (this.Mappers.ContainsKey(tuple.ToString()))
                    return this.Mappers[tuple.ToString()];
            }
            throw new MapperNotFoundException(sourceType, destinationType);
        }

        /// <summary>
        /// Gets the map method reference.
        /// </summary>
        /// <param name="source">The source type.</param>
        /// <param name="destination">The destination type.</param>
        private MethodInfo GetMapMethodInfo(Type source, Type destination)
        {
            return this.MapMethodInfo.MakeGenericMethod(source, destination);
        }
    }
}
